/**
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */
package org.evosuite.graphs.cfg;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.evosuite.Properties;
import org.evosuite.Properties.Criterion;
import org.evosuite.coverage.branch.BranchPool;
import org.evosuite.runtime.annotation.EvoSuiteExclude;
import org.evosuite.runtime.instrumentation.AnnotatedMethodNode;
import org.evosuite.instrumentation.coverage.BranchInstrumentation;
import org.evosuite.instrumentation.coverage.DefUseInstrumentation;
import org.evosuite.instrumentation.coverage.MethodInstrumentation;
import org.evosuite.instrumentation.coverage.MutationInstrumentation;
import org.evosuite.runtime.classhandling.ClassResetter;
import org.evosuite.setup.DependencyAnalysis;
import org.evosuite.utils.ArrayUtil;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Create a minimized control flow graph for the method and store it. In
 * addition, this adapter also adds instrumentation for branch distance
 * measurement
 * 
 * defUse, concurrency and LCSAJs instrumentation is also added (if the
 * properties are set).
 * 
 * @author Gordon Fraser
 */
public class CFGMethodAdapter extends MethodVisitor {

	private static final Logger logger = LoggerFactory.getLogger(CFGMethodAdapter.class);

	/**
	 * A list of Strings representing method signatures. Methods matching those
	 * signatures are not instrumented and no CFG is generated for them. Except
	 * if some MethodInstrumentation requests it.
	 */
	public static final List<String> EXCLUDE = Arrays.asList("<clinit>()V",
																ClassResetter.STATIC_RESET+"()V",
																ClassResetter.STATIC_RESET);
	/**
	 * The set of all methods which can be used during test case generation This
	 * excludes e.g. synthetic, initializers, private and deprecated methods
	 */
	public static Map<ClassLoader,Map<String, Set<String>>> methods = new HashMap<ClassLoader,Map<String, Set<String>>>();

	/**
	 * This is the name + the description of the method. It is more like the
	 * signature and less like the name. The name of the method can be found in
	 * this.plain_name
	 */
	private final String methodName;

	private final MethodVisitor next;
	private final String plain_name;
	private final int access;
	private final String className;
	private final ClassLoader classLoader;

	private int lineNumber = 0;
	
	/** Can be set by annotation */
	private boolean excludeMethod = false;

	/**
	 * <p>
	 * Constructor for CFGMethodAdapter.
	 * </p>
	 * 
	 * @param className
	 *            a {@link java.lang.String} object.
	 * @param access
	 *            a int.
	 * @param name
	 *            a {@link java.lang.String} object.
	 * @param desc
	 *            a {@link java.lang.String} object.
	 * @param signature
	 *            a {@link java.lang.String} object.
	 * @param exceptions
	 *            an array of {@link java.lang.String} objects.
	 * @param mv
	 *            a {@link org.objectweb.asm.MethodVisitor} object.
	 */
	public CFGMethodAdapter(ClassLoader classLoader, String className, int access,
	        String name, String desc, String signature, String[] exceptions,
	        MethodVisitor mv) {

		// super(new MethodNode(access, name, desc, signature, exceptions),
		// className,
		// name.replace('/', '.'), null, desc);

		super(Opcodes.ASM5, new AnnotatedMethodNode(access, name, desc, signature,
		        exceptions));

		this.next = mv;
		this.className = className; // .replace('/', '.');
		this.access = access;
		this.methodName = name + desc;
		this.plain_name = name;
		this.classLoader = classLoader;

		if(!methods.containsKey(classLoader))
			methods.put(classLoader, new HashMap<String, Set<String>>());
	}

	/* (non-Javadoc)
	 * @see org.objectweb.asm.MethodVisitor#visitLineNumber(int, org.objectweb.asm.Label)
	 */
	@Override
	public void visitLineNumber(int line, Label start) {
		lineNumber = line;
		super.visitLineNumber(line, start);
	}
	
	@Override
	public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
		if(Type.getDescriptor(EvoSuiteExclude.class).equals(desc)) {
			logger.info("Method has EvoSuite annotation: "+desc);
			excludeMethod = true;
		}
		return super.visitAnnotation(desc, visible);
	}

	/** {@inheritDoc} */
	@Override
	public void visitEnd() {
		logger.debug("Creating CFG of "+className+"."+methodName);
		boolean isExcludedMethod = excludeMethod || EXCLUDE.contains(methodName);
		boolean isMainMethod = plain_name.equals("main") && Modifier.isStatic(access);

		List<MethodInstrumentation> instrumentations = new ArrayList<MethodInstrumentation>();
		if (DependencyAnalysis.shouldInstrument(className, methodName)) {
		    if (ArrayUtil.contains(Properties.CRITERION, Criterion.DEFUSE)
		            || ArrayUtil.contains(Properties.CRITERION, Criterion.ALLDEFS)) {
				instrumentations.add(new BranchInstrumentation());
				instrumentations.add(new DefUseInstrumentation());
			}
		    else if (ArrayUtil.contains(Properties.CRITERION, Criterion.MUTATION)
		            || ArrayUtil.contains(Properties.CRITERION, Criterion.WEAKMUTATION)
		            || ArrayUtil.contains(Properties.CRITERION, Criterion.ONLYMUTATION)
		            || ArrayUtil.contains(Properties.CRITERION, Criterion.STRONGMUTATION)) {
				instrumentations.add(new BranchInstrumentation());
				instrumentations.add(new MutationInstrumentation());
			} else {
				instrumentations.add(new BranchInstrumentation());
			}
		} else {
			//instrumentations.add(new BranchInstrumentation());
		}

		boolean executeOnMain = false;
		boolean executeOnExcluded = false;

		for (MethodInstrumentation instrumentation : instrumentations) {
			executeOnMain = executeOnMain || instrumentation.executeOnMainMethod();
			executeOnExcluded = executeOnExcluded
			        || instrumentation.executeOnExcludedMethods();
		}

		// super.visitEnd();
		// Generate CFG of method
		MethodNode mn = (AnnotatedMethodNode) mv;

		boolean checkForMain = false;
		if (Properties.CONSIDER_MAIN_METHODS) {
			checkForMain = true;
		} else {
			checkForMain = !isMainMethod || executeOnMain;
		}

		// Only instrument if the method is (not main and not excluded) or (the
		// MethodInstrumentation wants it anyway)
		if (checkForMain && (!isExcludedMethod || executeOnExcluded)
		        && (access & Opcodes.ACC_ABSTRACT) == 0
		        && (access & Opcodes.ACC_NATIVE) == 0) {

			logger.info("Analyzing method " + methodName + " in class " + className);

			// MethodNode mn = new CFGMethodNode((MethodNode)mv);
			// System.out.println("Generating CFG for "+ className+"."+mn.name +
			// " ("+mn.desc +")");

			BytecodeAnalyzer bytecodeAnalyzer = new BytecodeAnalyzer();
			logger.info("Generating CFG for method " + methodName);

			try {

				bytecodeAnalyzer.analyze(classLoader, className, methodName, mn);
				logger.trace("Method graph for "
				        + className
				        + "."
				        + methodName
				        + " contains "
				        + bytecodeAnalyzer.retrieveCFGGenerator().getRawGraph().vertexSet().size()
				        + " nodes for " + bytecodeAnalyzer.getFrames().length
				        + " instructions");
				// compute Raw and ActualCFG and put both into GraphPool
				bytecodeAnalyzer.retrieveCFGGenerator().registerCFGs();
				logger.info("Created CFG for method " + methodName);

				if (DependencyAnalysis.shouldInstrument(className, methodName)) {
					if (!methods.get(classLoader).containsKey(className))
						methods.get(classLoader).put(className, new HashSet<String>());

					// add the actual instrumentation
					logger.info("Instrumenting method " + methodName + " in class "
					        + className);
					for (MethodInstrumentation instrumentation : instrumentations)
						instrumentation.analyze(classLoader, mn, className, methodName, access);

					handleBranchlessMethods();
					String id = className + "." + methodName;
					if (isUsable()) {
						methods.get(classLoader).get(className).add(id);
						logger.debug("Counting: " + id);
					}
				}
			} catch (AnalyzerException e) {
				logger.error("Analyzer exception while analyzing " + className + "."
				        + methodName + ": " + e);
				e.printStackTrace();
			}

		} else {
			logger.debug("NOT Creating CFG of "+className+"."+methodName+": "+checkForMain+", "+((!isExcludedMethod || executeOnExcluded)) +", "+((access & Opcodes.ACC_ABSTRACT) == 0)+", "+((access & Opcodes.ACC_NATIVE) == 0));
			super.visitEnd();
		}
		mn.accept(next);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.objectweb.asm.commons.LocalVariablesSorter#visitMaxs(int, int)
	 */
	/** {@inheritDoc} */
	@Override
	public void visitMaxs(int maxStack, int maxLocals) {
		int maxNum = 7;
		super.visitMaxs(Math.max(maxNum, maxStack), maxLocals);
	}

	private void handleBranchlessMethods() {
		String id = className + "." + methodName;
		if (BranchPool.getInstance(classLoader).getNonArtificialBranchCountForMethod(className, methodName) == 0) {
			if (isUsable()) {
				logger.debug("Method has no branches: " + id);
				BranchPool.getInstance(classLoader).addBranchlessMethod(className, id, lineNumber);
			}
		}
	}

	/**
	 * See description of CFGMethodAdapter.EXCLUDE
	 * 
	 * @return
	 */
	private boolean isUsable() {
		if((this.access & Opcodes.ACC_SYNTHETIC) != 0)
			return false;

		if((this.access & Opcodes.ACC_BRIDGE) != 0)
			return false;

		if((this.access & Opcodes.ACC_NATIVE) != 0)
			return false;

		if(methodName.contains("<clinit>"))
			return false;

		// If we are not using reflection, covering private constructors is difficult?
		if(Properties.P_REFLECTION_ON_PRIVATE <= 0.0) {
			if(methodName.contains("<init>") && (access & Opcodes.ACC_PRIVATE) == Opcodes.ACC_PRIVATE)
				return false;
		}

		return true;
	}
	
	public Set<String> getMethods(String className) {
		return getMethods(classLoader, className);
	}

	/**
	 * Returns a set with all unique methodNames of methods.
	 * 
	 * @return A set with all unique methodNames of methods.
	 * @param className
	 *            a {@link java.lang.String} object.
	 */
	public static Set<String> getMethods(ClassLoader classLoader, String className) {
		Set<String> targetMethods = new HashSet<String>();
		if(!methods.containsKey(classLoader))
			return targetMethods;
		
		for (String currentClass : methods.get(classLoader).keySet()) {
			if (currentClass.equals(className)
			        || currentClass.startsWith(className + "$"))
				targetMethods.addAll(methods.get(classLoader).get(currentClass));
		}

		return targetMethods;
	}
	
	public Set<String> getMethods() {
		return getMethods(classLoader);
	}

	/**
	 * Returns a set with all unique methodNames of methods.
	 * 
	 * @return A set with all unique methodNames of methods.
	 */
	public static Set<String> getMethods(ClassLoader classLoader) {
		Set<String> targetMethods = new HashSet<String>();
		if(!methods.containsKey(classLoader))
			return targetMethods;
		
		for (String currentClass : methods.get(classLoader).keySet()) {
			targetMethods.addAll(methods.get(classLoader).get(currentClass));
		}

		return targetMethods;
	}
	
	
	public Set<String> getMethodsPrefix(String className) {
		return getMethodsPrefix(classLoader, className);
	}

	/**
	 * Returns a set with all unique methodNames of methods.
	 * 
	 * @return A set with all unique methodNames of methods.
	 * @param className
	 *            a {@link java.lang.String} object.
	 */
	public static Set<String> getMethodsPrefix(ClassLoader classLoader, String className) {
		Set<String> matchingMethods = new HashSet<String>();
		if(!methods.containsKey(classLoader))
			return matchingMethods;

		for (String name : methods.get(classLoader).keySet()) {
			if (name.startsWith(className)) {
				matchingMethods.addAll(methods.get(classLoader).get(name));
			}
		}

		return matchingMethods;
	}
	
	public int getNumMethodsPrefix(String className) {
		return getNumMethodsPrefix(classLoader, className);
	}

	/**
	 * Returns a set with all unique methodNames of methods.
	 * 
	 * @return A set with all unique methodNames of methods.
	 * @param className
	 *            a {@link java.lang.String} object.
	 */
	public static int getNumMethodsPrefix(ClassLoader classLoader, String className) {
		int num = 0;
		if(!methods.containsKey(classLoader))
			return num;

		for (String name : methods.get(classLoader).keySet()) {
			if (name.startsWith(className)) {
				num += methods.get(classLoader).get(name).size();
			}
		}

		return num;
	}
	
	public int getNumMethods() {
		return getNumMethods(classLoader);
	}

	/**
	 * Returns a set with all unique methodNames of methods.
	 * 
	 * @return A set with all unique methodNames of methods.
	 */
	public static int getNumMethods(ClassLoader classLoader) {
		int num = 0;
		if(!methods.containsKey(classLoader))
			return num;
		
		for (String name : methods.get(classLoader).keySet()) {
			num += methods.get(classLoader).get(name).size();
		}

		return num;
	}
	
	
	public int getNumMethodsMemberClasses(String className) {
		return getNumMethodsMemberClasses(classLoader, className);
	}

	/**
	 * Returns a set with all unique methodNames of methods.
	 * 
	 * @return A set with all unique methodNames of methods.
	 * @param className
	 *            a {@link java.lang.String} object.
	 */
	public static int getNumMethodsMemberClasses(ClassLoader classLoader, String className) {
		int num = 0;
		if(!methods.containsKey(classLoader))
			return num;

		for (String name : methods.get(classLoader).keySet()) {
			if (name.equals(className) || name.startsWith(className + "$")) {
				num += methods.get(classLoader).get(name).size();
			}
		}

		return num;
	}
}
